package cc.verywell.pureblock.msg.nfs.map;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.nio.file.SimpleFileVisitor;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Stream;

import cc.verywell.pureblock.func.node.KeyShell;
import cc.verywell.pureblock.log.Pblog;
import cc.verywell.pureblock.msg.nfs.hd.LfsHdSupply;
import cc.verywell.pureblock.util.sign.Sha;

/**
* 
* capacity 如果是正数，就按传统的文件一共多大多小这么算，如果是负数, 就按磁盘剩余空间算。
* 
* 一般系统是4K一簇，不够4K的文件都会占用4K空间，建议用负数，这样比较省资源, 当然了;
* 
* 
* 
* 纪念科目一满分
* Copyright © 2019 TanYaqiu. All rights reserved.
* @author TanYaqiu 
* @address flat 601
* @date 2019年4月16日 上午11:59:56
*/
public class HDmap implements FSmap  {
	
	public static int DIRS_MAX = 1<<14; //Linux ext3 最大新建文件夹数为 31998
	public static int FILENAME_MAX = 128;
	public static long FILE_CLUSTOR = 4*1024; //这个是磁盘一簇的空间， 一般系统是按此为单位占用空间的, 对于一些压缩文件夹，请调成1
	
	final protected Path filePath;
	final private long capacity;
	final private boolean usesize;
	private long used;
	private ReentrantReadWriteLock initLock;
	//CountDownLatch cdl = new CountDownLatch(3);
	
	private Map<String,FileSize> sizemap;
	static private class FileSize{
		public AtomicInteger v = new AtomicInteger(0);
		public long size;
		FileSize(long size){this.size = size;}
		FileSize(){this(0);}
	}

	public HDmap(Path filePath,long capacity) {
		this.filePath = filePath;
		this.capacity = capacity;
		this.usesize = capacity>0;
		this.initLock = new ReentrantReadWriteLock();
		Map<String,FileSize> map = new LinkedHashMap<>(DIRS_MAX>>4,0.9f,true);
		this.sizemap = Collections.synchronizedMap(map);
		
		this.initLock.writeLock().lock();
		initRun();/// 请在异步执行
	}
	
	public void initRun() {
		long time = Long.MAX_VALUE;
		try {
			time = System.currentTimeMillis(); //这个也是有可能出错的;
			if(Files.isDirectory(filePath)) {
				try(Stream<Path> sm = Files.list(filePath);){ //列出
					sm.sorted((a,b)->{ //排序
						try {
							FileTime ta = Files.getLastModifiedTime(a);
							try {
								FileTime tb = Files.getLastModifiedTime(b);
								return ta.compareTo(tb);
							} catch (IOException e) {
								return 1;
							}
						} catch (IOException e) {
							if(Pblog.FS)Pblog.FS(e);
							return -1;
						}
						
					}).forEach(a->{ //入表
//						try {
//							Pblog.print("TEST", a," t:",Files.getLastModifiedTime(a));
//						} catch (IOException e1) {
//							e1.printStackTrace();
//						}
						FileSize fs;
						if(usesize) {
							try(Stream<Path> asm = Files.list(a);){ //否则不会释放目录
								long fsize = asm.mapToLong(b->{
									try {
										return getFileSize(b);//Files.size(b);
									} catch (IOException e) {
										if(Pblog.FS)Pblog.FS(e);
									}
									return 0;
								}).sum();
								fs = new FileSize(fsize);
							} catch (IOException e) {
								if(Pblog.FS)Pblog.FS(e);
								fs = new FileSize();
							}
							this.used += fs.size;
						}else {
							fs = new FileSize();
						}
						this.sizemap.put(a.getFileName().toString(), fs);
					});
				}catch(IOException e) {
					if(Pblog.FS)Pblog.FS(e);
				}
			}
		}finally {
			ReentrantReadWriteLock initLock = this.initLock;
			if(initLock!=null) {
				try {
					initLock.writeLock().unlock();
				}finally {
					this.initLock = null;
				}
			}
			if(Pblog.DEBUG)Pblog.DEBUG(this," init time: ",System.currentTimeMillis()-time);
		}
	}
	
	

	static final byte[] ZERObuffer = new byte[0];
	@Override
	public byte[] get(KeyShell key) {
		byte[] bb = hdget(key);
		if(bb!=ZERObuffer)return bb;
		return null;
	}
	public byte[] hdget(KeyShell key) {
		String dir = encodeDir(key);
		this.sizemap.get(dir);
		Path p = filePath.resolve(dir).resolve(encodeFilename(key));
		if(!Files.exists(p))return null;
		//cdl.countDown();try {cdl.await();
		//} catch (InterruptedException e1) {}
		//if(Pblog.FS)Pblog.FS("get go");
		try(AsynchronousFileChannel afc = AsynchronousFileChannel.open(p,StandardOpenOption.READ);){
			if(Pblog.DEBUG)Pblog.DEBUG("hdmap read ",p);
			long size = Files.size(p);
			if(0==size)return ZERObuffer;
			ByteBuffer bb = ByteBuffer.allocate((int)size);
			Future<Integer> f = afc.read(bb, 0);
//			if(Pblog.FS)Pblog.FS("gs500");
//			Thread.sleep(500);
//			if(Pblog.FS)Pblog.FS("ge500");
			f.get();//blocked
			bb.flip();
			{//验证, 这就是hash冲突了;
				if(!Arrays.equals(key.key, Sha.getInstance().hash(bb))) {
					if(Pblog.FS)Pblog.FS("key:",key," not equals file ",p," (",Sha.getInstance().hash(bb),")");
					return null;
				}
			}
			if(bb.array().length==bb.remaining())return bb.array();
			return Arrays.copyOfRange(bb.array(), bb.arrayOffset()+bb.position(), bb.limit());
		}catch(IOException e) {
			if(Files.exists(p)) {
				if(Pblog.FS)Pblog.FS(e);
			}
		} catch (InterruptedException e) {
			if(Pblog.FS)Pblog.FS(e);
		} catch (ExecutionException e) {
			if(Pblog.FS)Pblog.FS(e);
		}
		return null;
	}
	
	@Override
	public boolean contain(KeyShell key) {
		Path p = filePath.resolve(encodeDir(key)).resolve(encodeFilename(key));
		return Files.exists(p);
	}
	
	//////////////
	
	/**
	 * 
	 */
	private Map<KeyShell,Object> lockmap = new ConcurrentHashMap<>();
	
	public enum HDPUT{
		EXIST,
		NOSPACE,
		SUCCESS,
		ERROR,
		OTHER_SUCCESS,
		OTHER_FAILD;//另一线程可能是去删除的;
	}
	@Override
	public boolean put(KeyShell key, byte[] bb) {
		switch(hdput(key, bb)) {
		case EXIST:
		case SUCCESS:
		case OTHER_SUCCESS:
			return true;
		case ERROR:
		case NOSPACE:
		case OTHER_FAILD:
		}
		return false;
	}
	public HDPUT hdput(KeyShell key, byte[] bb) {
		ReentrantReadWriteLock initLock = this.initLock;
		if(initLock!=null) {
			try {
				initLock.readLock().lock();
			}finally {
				initLock.readLock().unlock();
			}
		}
		Object lock = new Object(),oldlock;
		String dir = encodeDir(key);
		Path d = filePath.resolve(dir);
		Path f = d.resolve(encodeFilename(key)); 
		synchronized (lock) {
			KeyShell k = key;
			oldlock = lockmap.putIfAbsent(k,lock);
			if( null == oldlock ) {
				try {
					//Pblog.print("TEST", f);
					if(Files.exists(f))return HDPUT.EXIST; // 通常是因为 文件名重合了;
					long need = bb.length;
					//Pblog.print("TEST", need,"<>",restSpace());
					if(need>restSpace()) {
						HashSet<String> deleted = new HashSet<String>();
						do{
							Map.Entry<String,FileSize> entry = null;
							ss:synchronized (sizemap) {
								Iterator<Map.Entry<String,FileSize>> it = sizemap.entrySet().iterator();
								while(it.hasNext()) {
									entry = it.next();
									if(deleted.add(entry.getKey())) {
										entry.getValue().v.getAndIncrement();
										break ss;
									}else {
										entry = null;
									}
								}
							}
							if(entry!=null) {
								if(!freeDir(entry.getKey(), entry.getValue())) { //如果目录不能删除
									sizemap.get(entry.getKey()); //就把它放在accessOrder的最后面;
								}
							}else {
								return HDPUT.NOSPACE;
							}
						}while(need>restSpace());
					}
					FileSize fs = new FileSize();
					synchronized (sizemap) {
						FileSize fsold = sizemap.putIfAbsent(dir, fs);
						if(fsold!=null) {
							fs = fsold;
						}
						fs.v.getAndIncrement();
					}
					////在此期间 fs 可能已被删除
					synchronized (fs) { //在目录同步内进行目录操作;
						fs.v.getAndDecrement();
						if(Files.exists(f))return HDPUT.EXIST;
						if(Pblog.DEBUG)Pblog.DEBUG("hdmap put ",f);
						for(int i = 0;i<10;i++) {
							Path temp = null;
							try {
								Files.createDirectories(d);
								if(usesize) {
									long p;
									temp = Files.createTempFile(d,"tmp-",".d");
									try(AsynchronousFileChannel afc = AsynchronousFileChannel.open(temp,StandardOpenOption.CREATE,StandardOpenOption.WRITE);){
										Future<Integer> fu = afc.write(ByteBuffer.wrap(bb), 0);
										/*p = */fu.get();
									}
									p = getFileSize(temp);
									//Path f = d.resolve(encodeFilename(key)); 
									Files.move(temp, f/*, StandardCopyOption.REPLACE_EXISTING*/);
									temp = null;
									fs.size += p;
									synchronized(HDmap.this) {
										this.used += p;
									}
								}else {
									temp = Files.createTempFile(d,"tmp-",".d");
									try(AsynchronousFileChannel afc = AsynchronousFileChannel.open(temp,StandardOpenOption.CREATE,StandardOpenOption.WRITE);){
										Future<Integer> fu = afc.write(ByteBuffer.wrap(bb), 0);
										fu.get();
									}
									Files.move(temp, f);
									temp = null;
								}
								return HDPUT.SUCCESS;
							}catch(IOException e1) {
								if(e1.getMessage().indexOf("many open file")<0) { //什么鬼，谁能解析一下？ 如何在系统排队?
									throw e1;
								}
							}finally {
								if(temp!=null) {
									try {
										Files.deleteIfExists(temp);
									} catch (IOException e) {
										if(Pblog.FS)Pblog.FS(e);
									}
								}
							}
							Thread.sleep(i*100);
						}
					}
//					if(Pblog.FS)Pblog.FS("ps500");
//					Thread.sleep(500);
//					if(Pblog.FS)Pblog.FS("pe500");
					return HDPUT.ERROR;
				} catch (IOException e) {
					if(Pblog.FS)Pblog.FS(e);
				} catch (InterruptedException | ExecutionException e) {
					if(Pblog.DEBUG)Pblog.DEBUG(e);
				}finally {
					lockmap.remove(k);
				}
			}
		}
		key = null; //取消引用
		bb = null; //取消引用
		if(oldlock!=null) {
			synchronized (oldlock) {
				return Files.exists(f)?HDPUT.OTHER_SUCCESS:HDPUT.OTHER_FAILD;
			}
		}
		return HDPUT.ERROR;
	}

	public enum HDREMOVE{
		NOEXIST,
		SUCCESS,
		ERROR,
		OTHER_SUCCESS,
		OTHER_FAILD,//另一线程可能是去删除的;
		HOLDING;
	}
	@Override
	public boolean remove(KeyShell key) {
		switch(hdremove(key)) {
		case NOEXIST:
		case SUCCESS:
		case OTHER_SUCCESS:
			return true;
		case ERROR:
		case OTHER_FAILD:
		case HOLDING:
		}
		return false;
	}
	public HDREMOVE hdremove(KeyShell key) {
		ReentrantReadWriteLock initLock = this.initLock;
		if(initLock!=null) {
			try {
				initLock.readLock().lock();
			}finally {
				initLock.readLock().unlock();
			}
		}
		//Pblog.print("TEST","remove ", key);
		Object lock = new Object(),oldlock;
		String dir = encodeDir(key);
		Path d = filePath.resolve(dir);
		Path f = d.resolve(encodeFilename(key)); 
		synchronized (lock) {
			KeyShell k = key;
			oldlock = lockmap.putIfAbsent(k,lock);
			if( null == oldlock ) {
				try {
					FileSize fs = new FileSize();
					synchronized (sizemap) {
						FileSize fsold = this.sizemap.putIfAbsent(dir, fs);
						if(fsold!=null) {
							fs = fsold;
						}
						fs.v.getAndIncrement();
					}
					synchronized (fs) { //在目录同步内进行目录操作;
						fs.v.getAndDecrement();
						//Pblog.print("TEST","dir ", dir);
						//Pblog.print("TEST","remove ", d);
						if(!Files.exists(d))return HDREMOVE.NOEXIST;
						if(isHolding(key))return HDREMOVE.HOLDING;
//						cdl.countDown();try {cdl.await();
//						} catch (InterruptedException e1) {}
//						if(Pblog.FS)Pblog.FS("del go");
						if(Pblog.DEBUG)Pblog.DEBUG("hdmap remove ",f);
						for(int i = 0;i<10;i++) {
							try {
								if(usesize) {
									long p = getFileSize(f);//Files.size(f); //java.nio.file.NoSuchFileException
									Files.delete(f);
									fs.size -= p;
									try {
										Files.delete(d);
										p += fs.size;
										synchronized (sizemap) {
											if(fs.v.get()==0) {
												this.sizemap.remove(dir);
											}
										}
									} catch (DirectoryNotEmptyException  e) {}
									synchronized(HDmap.this) {
										this.used -= p;
									}
								}else {
//									long p = getFileSize(f);//Files.size(f); //java.nio.file.NoSuchFileException
									Files.delete(f);
//									fs.size -= p;
									try {
										Files.delete(d);
//										p += fs.size;
										synchronized (sizemap) {
											if(fs.v.get()==0) {
												this.sizemap.remove(dir);
											}
										}
									} catch (DirectoryNotEmptyException  e) {}
//									synchronized(HDmap.this) {
//										this.used -= p;
//									}
								}
								return HDREMOVE.SUCCESS;
							}catch(IOException e1) {
								if(e1.getMessage().indexOf("many open file")<0) { //什么鬼，谁能解析一下？ 如何在系统排队?
									throw e1;
								}
							}
							Thread.sleep(i*100);
						}
//						if(Pblog.FS)Pblog.FS("rs300");
//						Thread.sleep(300);
//						if(Pblog.FS)Pblog.FS("re300");
					}
					return HDREMOVE.ERROR;
				} catch (FileNotFoundException e) {
					return HDREMOVE.NOEXIST;
				} catch (IOException e) {
					if(Pblog.FS)Pblog.FS(e);
				} catch (InterruptedException e) {
					if(Pblog.DEBUG)Pblog.DEBUG(e);
				}finally {
					lockmap.remove(k);
				}
			}
		}
		key = null; //取消引用
		if(oldlock!=null) {
			synchronized (oldlock) {
				return Files.exists(f)?HDREMOVE.OTHER_FAILD:HDREMOVE.OTHER_SUCCESS;
			}
		}
		return HDREMOVE.ERROR;
	}
	
	boolean freeDir(String dir,FileSize fs) {
//		if(Pblog.DEBUG)Pblog.DEBUG("fs.size ",fs.size);
		synchronized (fs) {
			int i = fs.v.decrementAndGet();
			if(i>0)return false;
			Path d = filePath.resolve(dir);
			if(Pblog.DEBUG)Pblog.DEBUG("hdmap freeDir ",d);
			DelWalk walk = new DelWalk(usesize?fs:null);
			try {
				if(Files.exists(d)) {
					Files.walkFileTree(d, walk);
				}
			} catch (IOException e) {
				if(Pblog.FS)Pblog.FS(e);
				if(walk.err==null)walk.err=e;
			}finally {
				if(usesize) {
					synchronized(HDmap.this) {
//						if(Pblog.DEBUG)Pblog.DEBUG("used ",used);
//						if(Pblog.DEBUG)Pblog.DEBUG("totalDeleted ",walk.totalDeleted);
//						if(Pblog.DEBUG)Pblog.DEBUG("fs.size ",fs.size);
						this.used -= walk.totalDeleted;
						if(walk.err==null) {
							this.used -= fs.size;
							fs.size = 0;
						}
					}
				}
				if(walk.err==null) {
					synchronized (sizemap) {
						if(fs.v.get()==0)
							sizemap.remove(dir);
					}

				}
			}
			return walk.err==null;
		}
	}


	////////////////////////////////////////////////////////////////////
	/**
	 * 可重载，如果文件名太短，或许需要数据库作二级支撑
	 */
	public String encodeDir(KeyShell key) {
		int hash = Arrays.hashCode(key.key) % DIRS_MAX;
		if(hash<0)hash=~hash;
		return Integer.toHexString(hash);
	}
	/**
	 * 可重载，如果文件名太短，或许需要数据库作二级支撑 
	 */
	public String encodeFilename(KeyShell key) {
		String str = Pblog.byteArray2Hex(key.key);
		if(str.length()>FILENAME_MAX){
			int c = str.length()-FILENAME_MAX;
			int d = c/2;
			str = str.substring(d, str.length()-(c-d));
		}
		return str+".d";
	}
	protected long restSpace() throws IOException {
		long restspace = 0;
		Path p = filePath;
		while(p!=null && !Files.exists(p)) {
			p = p.getParent();
		}
		long freespace = Files.getFileStore(p).getUsableSpace();
		if(usesize) {
			synchronized(HDmap.this) {
				restspace = capacity - this.used;
			}
			if(restspace>freespace)
				restspace=freespace;
		}else{
			restspace = freespace+capacity;
		}
		return restspace;
	}
	protected static long getFileSize(Path p) throws IOException{
		long size = Files.size(p);
		if(size==0)return size;
		return ((size-1)/FILE_CLUSTOR+1)*FILE_CLUSTOR;
	}
	
	final private static IOException HAS_HOLDING = new IOException("hasHolding");
	class DelWalk extends SimpleFileVisitor<Path> {
		private FileSize fs;
		public long totalDeleted = 0;
		public IOException err = null;
		public DelWalk(FileSize fs) {
			this.fs = fs;
		}
		private boolean hasHoldingDirectory = false;
		
		@Override
		public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
			//Pblog.print("TEST", "del: ",file);
			if(isHolding(file)) {
				hasHoldingDirectory = true;
			}else if(fs==null) {
				Files.delete(file);
			}else {
				long size =  getFileSize(file);//Files.size(file);
				Files.delete(file);
				fs.size -= size;
				totalDeleted += size;
			}
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
			if (exc == null) {
				//Pblog.print("TEST", "del: ",dir);
				if(hasHoldingDirectory) {
					hasHoldingDirectory = false;
					if(err==null)err = HAS_HOLDING;
				}else {
					Files.deleteIfExists(dir);
				}
			} else {
				err = exc;
				throw exc;
			}
			return FileVisitResult.CONTINUE;
		}
	}
	
	@Override
	public String toString() {
		StringBuffer sb = new StringBuffer();
		synchronized(sizemap) {
			sb.append("HDmap dirs:").append(sizemap.size()).append("/").append(DIRS_MAX);
			//sizemap.keySet().stream().forEach(a->{sb.append(" ").append(a);});
		}
		if(usesize) {
			synchronized(this) {
				sb.append(" used:").append(LfsHdSupply.formatSize(used)).append("/").append(LfsHdSupply.formatSize(capacity));
			}
		}else {
			sb.append(" rest:");
			try {
				long rest = restSpace();
				sb.append(LfsHdSupply.formatSize(rest)).append(" diskUnused: ").append(LfsHdSupply.formatSize(rest-capacity)).append('(').append(rest-capacity).append(')');
			} catch (IOException e) {
				sb.append(e);
			}
		}
		return sb.toString();
	}
	
	public long capacity() throws IOException {
		if(usesize) {
			long  freespace;
			Path p = filePath;
			while(p!=null && !Files.exists(p)) {
				p = p.getParent();
			}
			freespace = Files.getFileStore(p).getUsableSpace();
			synchronized(this) {
				return Math.min(capacity-used,freespace)+used;
			}
		}else{
			long totalspace;
			Path p = filePath;
			while(p!=null && !Files.exists(p)) {
				p = p.getParent();
			}
			totalspace = Files.getFileStore(p).getTotalSpace();
			return totalspace+capacity;
		}
	}
	protected boolean isHolding(KeyShell key) {return false;}
	protected boolean isHolding(Path file) {return false;}
	
 }
